Skip to content

refactor: extract shared UI components (Phase 08)#822

Merged
reachrazamair merged 4 commits intoRunMaestro:rcfrom
jSydorowicz21:dedup/phase-08-shared-ui-components
Apr 16, 2026
Merged

refactor: extract shared UI components (Phase 08)#822
reachrazamair merged 4 commits intoRunMaestro:rcfrom
jSydorowicz21:dedup/phase-08-shared-ui-components

Conversation

@jSydorowicz21
Copy link
Copy Markdown
Contributor

@jSydorowicz21 jSydorowicz21 commented Apr 13, 2026

Summary

Extracts three shared UI primitives and migrates existing call sites. Net effect: one canonical pattern per primitive instead of 140+ hand-rolled variants.

Net: +223 lines overall (new components + tests), but -178 lines in migrated call sites (3 new component files + tests account for the delta)

08A - GhostIconButton

New: src/renderer/components/ui/GhostIconButton.tsx + test.

The standard "icon-only button with hover bg and optional tooltip" pattern used 100+ times across the renderer.

  • Migrated: 54 call sites across 43 files (AST-aware codemod handled className drop, aria-label -> ariaLabel, style={{color:X}} -> color={X})
  • Skipped: sites with extra layout classes (flex variants, padding variants, focus rings), non-icon children with custom styles - would require adding 3-4 edge-case props for 1-2 callers each

08B - Spinner

New: src/renderer/components/ui/Spinner.tsx + test.

The standard animated loading indicator wrapping Loader2 from lucide-react.

  • Migrated: 84 call sites across 55 files (44 simple + 40 color-styled variants)
  • Loader2 imports removed: 42 files
  • Skipped: 3 files had local components named Spinner that did something different (CSS-driven circular progress) - renamed those to CircularSpinner to resolve the name collision

08C - EmptyStatePlaceholder

New: src/renderer/components/ui/EmptyStatePlaceholder.tsx + test.

A generic placeholder for "No X" / "No results" / "Select something" states. Intentionally distinct from the existing EmptyStateView.tsx, which is the specialized welcome-screen component - different purpose, would have needed a confusing mode prop.

  • Adopted: 2 sites (AgentSessionsBrowser, AgentSessionsModal)
  • Skipped: most other empty-state occurrences had custom headings/actions/layouts that would bloat the component's prop surface for 1-2 callers

Test plan

  • npm run lint passes (all 3 tsconfigs)
  • npx prettier --check . passes
  • 109 UI component tests pass (13 new + existing)
  • 268 migrated-component tests pass (AboutModal, AgentSessionsModal, AgentSessionsBrowser, UpdateCheckModal)
  • Visual regression: hover any ghost icon button - bg should still appear
  • Visual regression: trigger a loading state - spinner should still animate
  • Visual regression: open Agent Sessions Browser with no sessions - placeholder should render

Risk

Low. Behavior-preserving migration: each call site produces identical DOM/styling. Three new components with dedicated tests. One intentional name-rename (Spinner -> CircularSpinner in 3 files) for the collision case.

Cross-PR interaction

This PR modifies AgentSessionsModal.tsx, which is deleted by #791 (Phase 01A). Whichever merges second will need a trivial conflict resolution (this PR's edits to that file will be dropped if 01A is already in).

Summary by CodeRabbit

  • New Features

    • Added three reusable UI primitives: Spinner, GhostIconButton, and EmptyStatePlaceholder.
  • Refactor

    • Replaced many inline loading icons and icon-only buttons with the new Spinner and GhostIconButton.
    • Standardized empty-state presentations across the app using EmptyStatePlaceholder.
  • Tests

    • Added component tests for EmptyStatePlaceholder, GhostIconButton, and Spinner; updated an empty-state test message.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds three UI primitives—Spinner, GhostIconButton, and EmptyStatePlaceholder—with tests and barrel exports, and replaces many inline loader icons and icon-only buttons across the renderer to use these shared components and props.

Changes

Cohort / File(s) Summary
New UI primitives & barrel
src/renderer/components/ui/Spinner.tsx, src/renderer/components/ui/GhostIconButton.tsx, src/renderer/components/ui/EmptyStatePlaceholder.tsx, src/renderer/components/ui/index.ts
Adds Spinner, GhostIconButton, and EmptyStatePlaceholder components and their prop types; re-exports them from the ui barrel.
Component tests
src/__tests__/renderer/components/ui/EmptyStatePlaceholder.test.tsx, src/__tests__/renderer/components/ui/GhostIconButton.test.tsx, src/__tests__/renderer/components/ui/Spinner.test.tsx
Adds Vitest + RTL tests covering rendering, props, accessibility, and event behavior for the new primitives.
Global replacement: spinners
src/renderer/components/... (examples: AboutModal.tsx, AgentSessionsBrowser.tsx, FeedbackChatView.tsx, MarketplaceModal.tsx, MergeProgressModal.tsx, RightPanel.tsx, ToolCallCard.tsx, UpdateCheckModal.tsx, etc.)
Replaces lucide-react Loader2 + animate-spin usages with the new Spinner component across many loading/status indicators, mapping sizes and colors to Spinner props.
Global replacement: icon buttons
src/renderer/components/... (examples: Modal.tsx, CreatePRModal.tsx, AgentCreationDialog.tsx, FilePreview/ImageViewer.tsx, SessionList/SessionList.tsx, Settings/*, PlaygroundPanel.tsx, etc.)
Replaces inline icon-only <button> elements with GhostIconButton, centralizing padding, color, aria/title, stopPropagation, and disabled behavior.
Empty-state adoption
src/renderer/components/AgentSessionsBrowser.tsx, src/renderer/components/AgentSessionsModal.tsx, ...
Introduces EmptyStatePlaceholder for zero-result/empty list UIs, replacing bespoke empty-state markup.
Local renames & small refactors
src/renderer/components/*Progress*.tsx, TransferProgressModal.tsx, SummarizeProgressModal.tsx
Minor local renames (e.g., local SpinnerCircularSpinner) and consistent adoption of shared primitives for close/cancel controls and active-stage visuals.

Sequence Diagram(s)

(Skipped — UI primitive additions and widespread replacements do not introduce a new multi-component sequential control flow requiring visualization.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

approved

Poem

🐰 I hopped in quick with tidy flair,
Spinners twirl and buttons care.
Empty frames now cozy, clear,
Shared bits hum — the UI cheer.
A carrot clap — code looks dear!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'refactor: extract shared UI components (Phase 08)' clearly and accurately summarizes the main change: refactoring to extract and consolidate shared UI components into reusable primitives.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jSydorowicz21 jSydorowicz21 force-pushed the dedup/phase-08-shared-ui-components branch from 3d4c050 to eb0eb0b Compare April 13, 2026 19:41
@jSydorowicz21 jSydorowicz21 self-assigned this Apr 14, 2026
@jSydorowicz21 jSydorowicz21 marked this pull request as ready for review April 14, 2026 04:35
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 14, 2026

Greptile Summary

This PR extracts three shared UI primitives — GhostIconButton, Spinner, and EmptyStatePlaceholder — from 100+ hand-rolled variants across the renderer, migrating 54, 84, and 2 call sites respectively. The migration is behavior-preserving and the new components are well-tested; all remaining findings are P2 style/consistency concerns.

Confidence Score: 5/5

  • Safe to merge — no correctness or data-integrity issues; all findings are P2 style suggestions.
  • All three new components are tested, the migration is behavior-preserving, and no P0/P1 defects were found. The only concerns are residual Loader2 usages in a few partially-migrated files and a triplicated CircularSpinner that could have been extracted, both of which are non-blocking.
  • AgentSessionsBrowser.tsx and AgentSessionsModal.tsx have mixed Spinner/Loader2 usage; MergeProgressModal.tsx, SummarizeProgressModal.tsx, and TransferProgressModal.tsx each define an identical CircularSpinner.

Important Files Changed

Filename Overview
src/renderer/components/ui/GhostIconButton.tsx New shared primitive; clean forwardRef implementation with well-typed props (stopPropagation, color, testId, etc.). No issues.
src/renderer/components/ui/Spinner.tsx New shared Spinner wrapping Loader2; solid implementation but lacks a testId prop (unlike GhostIconButton) which couples tests to the internal icon name.
src/renderer/components/ui/EmptyStatePlaceholder.tsx New generic empty-state primitive; clean implementation with sensible defaults and padding customization. No issues.
src/renderer/components/AgentSessionsBrowser.tsx Partially migrated — imports Spinner and GhostIconButton but retains two direct Loader2+animate-spin usages at lines 1117 and 1266, leaving mixed patterns in one file.
src/renderer/components/MergeProgressModal.tsx Spinner renamed to CircularSpinner (collision fix); identical implementation also in SummarizeProgressModal and TransferProgressModal — triplication missed as extraction opportunity.
src/renderer/components/ui/index.ts Barrel export updated cleanly with all three new primitives and their types. No issues.
src/tests/renderer/components/ui/GhostIconButton.test.tsx Good coverage: renders, click, disabled, custom padding, and stopPropagation scenarios all tested. No issues.
src/tests/renderer/components/ui/Spinner.test.tsx Tests size, color, className — but find the spinner via getByTestId('loader2-icon') which couples to the mock's icon name; no test for role/aria-label attributes.
src/tests/renderer/components/ui/EmptyStatePlaceholder.test.tsx Covers all four prop combinations (title-only, with icon, description, action). No issues.
src/renderer/components/AgentSessionsModal.tsx Same partial-migration pattern as AgentSessionsBrowser — imports Spinner but still uses Loader2 directly at line 514.
src/renderer/components/UpdateCheckModal.tsx Fully migrated to GhostIconButton and Spinner; Loader2 removed; no issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph "New Shared Primitives (ui/)"
        GIB["GhostIconButton\n(forwardRef, stopPropagation,\ncolor, padding, testId)"]
        SP["Spinner\n(Loader2 + animate-spin,\nsize, color, ariaLabel)"]
        ESP["EmptyStatePlaceholder\n(icon, title, description,\naction, padding props)"]
        IDX["ui/index.ts\n(barrel export)"]
    end

    subgraph "54 migrated call sites"
        MOD["Modal.tsx (header X btn)"]
        ASB["AgentSessionsBrowser.tsx\n(partial: 4 GhostIconButtons +\n2 residual Loader2)"]
        ASM["AgentSessionsModal.tsx\n(partial: residual Loader2)"]
        OTH["41 other files"]
    end

    subgraph "84 migrated Spinner sites"
        UCM["UpdateCheckModal.tsx (full)"]
        SYM["SymphonyModal.tsx\n(partial: residual Loader2)"]
        NP["NotificationsPanel.tsx\n(partial: residual Loader2)"]
        OTH2["51 other files"]
    end

    subgraph "2 EmptyStatePlaceholder sites"
        ASB2["AgentSessionsBrowser.tsx"]
        ASM2["AgentSessionsModal.tsx"]
    end

    subgraph "Renamed (collision resolution)"
        MER["MergeProgressModal\nSpinner→CircularSpinner"]
        SUM["SummarizeProgressModal\nSpinner→CircularSpinner"]
        TRA["TransferProgressModal\nSpinner→CircularSpinner"]
    end

    IDX --> GIB
    IDX --> SP
    IDX --> ESP

    GIB --> MOD
    GIB --> ASB
    GIB --> ASM
    GIB --> OTH

    SP --> UCM
    SP --> SYM
    SP --> NP
    SP --> OTH2

    ESP --> ASB2
    ESP --> ASM2

    SP -.->|"could replace"| MER
    SP -.->|"could replace"| SUM
    SP -.->|"could replace"| TRA
Loading

Comments Outside Diff (2)

  1. src/renderer/components/AgentSessionsBrowser.tsx, line 10 (link)

    P2 Residual Loader2 alongside Spinner in same file

    Loader2 is still imported and used directly at lines 1117 and 1266 (with animate-spin), while Spinner is imported and used at other sites in the same file. This leaves a mixed pattern where the new abstraction is partially applied. The two direct Loader2 usages could be replaced with <Spinner size={20} className="mx-auto" ... /> and <Spinner size={12} className="ml-auto" ... /> respectively.

    The same pattern applies in AgentSessionsModal.tsx (line 514), NotificationsPanel.tsx (line 399), and SymphonyModal.tsx (line 2049).

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  2. src/renderer/components/MergeProgressModal.tsx, line 98-113 (link)

    P2 Identical CircularSpinner defined three times

    The same CSS-based circular spinner (border-spin with a centered Wand2 icon) is defined verbatim in MergeProgressModal.tsx, SummarizeProgressModal.tsx, and TransferProgressModal.tsx. The PR renamed these from Spinner to CircularSpinner to resolve the naming collision, but didn't consolidate them into a shared component. Given this PR is specifically adding shared UI primitives, this was a natural candidate to extract alongside Spinner.

Reviews (1): Last reviewed commit: "refactor: extract shared UI components (..." | Re-trigger Greptile

Comment on lines +31 to +46
export function Spinner({
size = 16,
color,
className = '',
style,
ariaLabel = 'Loading',
}: SpinnerProps) {
return (
<Loader2
className={`animate-spin ${className}`.trim()}
style={{ width: size, height: size, color, ...style }}
aria-label={ariaLabel}
role="status"
/>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No testId prop unlike GhostIconButton

GhostIconButton exposes a testId prop (data-testid={testId}), but Spinner doesn't. Any test that needs to locate a Spinner must reach through the implementation detail (data-testid="loader2-icon" from the lucide mock), which will silently break if the underlying icon ever changes. Adding a testId prop (or a stable role="status" query approach) would keep test selectors decoupled from internal icon choices.

Suggested change
export function Spinner({
size = 16,
color,
className = '',
style,
ariaLabel = 'Loading',
}: SpinnerProps) {
return (
<Loader2
className={`animate-spin ${className}`.trim()}
style={{ width: size, height: size, color, ...style }}
aria-label={ariaLabel}
role="status"
/>
);
}
export interface SpinnerProps {
/** Icon size in pixels. Defaults to 16 */
size?: number;
/** Optional color (applied via inline style). */
color?: string;
/** Additional class names */
className?: string;
/** Inline style overrides */
style?: CSSProperties;
/** Accessible label. Defaults to 'Loading' */
ariaLabel?: string;
/** Test id for automated tests */
testId?: string;
}
export function Spinner({
size = 16,
color,
className = '',
style,
ariaLabel = 'Loading',
testId,
}: SpinnerProps) {
return (
<Loader2
className={`animate-spin ${className}`.trim()}
style={{ width: size, height: size, color, ...style }}
aria-label={ariaLabel}
role="status"
data-testid={testId}
/>
);
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/components/SymphonyModal.tsx (1)

712-719: ⚠️ Potential issue | 🟡 Minor

Remove unreachable loading indicator in the Available Issues header.

Line 718 is unreachable in this branch: Line 671 already routes isLoadingIssues === true to the skeleton path, so this conditional spinner never renders.

Suggested cleanup
-									{isLoadingIssues && <Spinner size={12} color={theme.colors.accent} />}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SymphonyModal.tsx` around lines 712 - 719, The header
for "Available Issues" contains an unreachable loading indicator: remove the
conditional JSX `{isLoadingIssues && <Spinner size={12}
color={theme.colors.accent} />}` in the h4 (the Available Issues header) because
`isLoadingIssues` is already routed to the skeleton path earlier; update the
component (SymphonyModal.tsx) to simply render the header text and count
(availableIssues.length) without that spinner conditional.
🧹 Nitpick comments (6)
src/renderer/components/ui/GhostIconButton.tsx (1)

31-32: Add an accessibility fallback for icon-only buttons.

If ariaLabel is omitted, this can render an unlabeled icon button. Fallback to title helps keep controls accessible with existing call sites.

Proposed fix
-				aria-label={ariaLabel}
+				aria-label={ariaLabel ?? title}

Also applies to: 92-92

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/ui/GhostIconButton.tsx` around lines 31 - 32, The
GhostIconButton component currently allows an icon-only button to be rendered
without an accessible name; update the rendered element to set aria-label to a
fallback value by using the props ariaLabel || title (i.e., when ariaLabel is
absent use title) so the button always has an accessible name, and ensure the
title prop is preserved on the element; locate GhostIconButton and change where
aria-label is assigned to use ariaLabel ?? title (or ariaLabel || title) for
both occurrences referenced (around the ariaLabel prop and the other usage).
src/renderer/components/SendToAgentModal.tsx (1)

18-19: Consider completing spinner standardization in this modal.

Line 19 introduces the shared Spinner, but the send CTA still uses Loader2 (Line 721). Converging on one loader component here will keep UI behavior/styles consistent and simplify imports.

♻️ Suggested patch
-import { Search, ArrowRight, X, Loader2, Circle } from 'lucide-react';
+import { Search, ArrowRight, X, Circle } from 'lucide-react';
@@
 						{isSending ? (
 							<>
-								<Loader2 className="w-4 h-4 animate-spin" aria-hidden="true" />
+								<Spinner size={16} />
 								Sending...
 							</>
 						) : (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SendToAgentModal.tsx` around lines 18 - 19, The send
CTA in SendToAgentModal mixes two loader components—use the shared Spinner
instead of Loader2 to standardize UI; update the send button render path in the
SendToAgentModal component to import and render Spinner (already imported at
top) wherever Loader2 is used (e.g., the send CTA/submit handler render branch),
remove the Loader2 usage and any related imports, and ensure Spinner receives
the same props (size/weight/className) used by Loader2 so styling and behavior
remain consistent.
src/renderer/components/Settings/EnvVarsEditor.tsx (1)

194-201: Prefer ariaLabel on the icon-only remove control.

Line 194 is functionally correct; adding ariaLabel makes the remove action more reliably announced by screen readers.

♿ Suggested patch
 								<GhostIconButton
 									onClick={() => removeEntry(entry.id)}
 									padding="p-2"
 									title="Remove variable"
+									ariaLabel={`Remove variable ${entry.key || ''}`.trim()}
 									color={theme.colors.textDim}
 								>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/Settings/EnvVarsEditor.tsx` around lines 194 - 201,
The GhostIconButton used for removing env var entries (the element rendering
Trash2 with onClick={() => removeEntry(entry.id)}) is icon-only and should
include an accessible label; add an ariaLabel prop (e.g., ariaLabel="Remove
variable") to the GhostIconButton so screen readers reliably announce the
action, keeping the existing title and styling intact and ensuring the
removeEntry(entry.id) handler remains unchanged.
src/renderer/components/DocumentsPanel.tsx (1)

488-490: Add an explicit accessible label to the icon-only close button.

Line 488 currently renders only an icon; adding ariaLabel (and optionally title) makes the control clearer for assistive tech.

♿ Suggested patch
-						<GhostIconButton onClick={onClose} color={theme.colors.textDim}>
+						<GhostIconButton
+							onClick={onClose}
+							color={theme.colors.textDim}
+							ariaLabel="Close document selector"
+							title="Close"
+						>
 							<X className="w-4 h-4" />
 						</GhostIconButton>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/DocumentsPanel.tsx` around lines 488 - 490, The close
button is icon-only and needs an accessible name: update the GhostIconButton
usage (the one with onClose and the X icon) to pass an explicit accessible label
like aria-label="Close" (and optional title="Close") so screen readers can
announce the control; ensure the props are added to the GhostIconButton
component invocation that currently renders <GhostIconButton onClick={onClose}
color={theme.colors.textDim}> with the X icon inside.
src/renderer/components/PlaygroundPanel.tsx (1)

599-601: Add an accessible name to the playground close icon button.

Line 599 is icon-only; include ariaLabel so assistive tech can announce the action reliably.

♿ Suggested patch
-						<GhostIconButton onClick={onClose} color={theme.colors.textDim}>
+						<GhostIconButton
+							onClick={onClose}
+							color={theme.colors.textDim}
+							ariaLabel="Close developer playground"
+							title="Close"
+						>
 							<X className="w-5 h-5" />
 						</GhostIconButton>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/PlaygroundPanel.tsx` around lines 599 - 601, The
close button in PlaygroundPanel is icon-only (GhostIconButton with X) and needs
an accessible name; update the GhostIconButton instantiation in
PlaygroundPanel.tsx to pass an accessible-name prop (e.g., ariaLabel or
aria-label depending on the GhostIconButton prop API) such as "Close playground"
and keep the existing onClose and color props so assistive tech can announce the
action reliably.
src/renderer/components/AutoRun/AutoRunExpandedModal.tsx (1)

422-424: Consider adding ariaLabel explicitly on the close icon button.

At Line 422, this currently relies on title for naming. Adding ariaLabel improves consistency and avoids depending on tooltip semantics for accessible naming.

Suggested refinement
-						<GhostIconButton onClick={handleClose} title="Close (Esc)">
+						<GhostIconButton
+							onClick={handleClose}
+							title="Close (Esc)"
+							ariaLabel="Close Auto Run expanded dialog"
+						>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AutoRun/AutoRunExpandedModal.tsx` around lines 422 -
424, The close button relies on a title for accessible naming; update the
GhostIconButton instance used with the onClick handler handleClose and icon X to
include an explicit ariaLabel prop (e.g., ariaLabel="Close") so screen readers
get a consistent name rather than depending on the tooltip/title; ensure the
ariaLabel string matches other close buttons in the component for consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/__tests__/renderer/components/ui/Spinner.test.tsx`:
- Around line 12-32: Tests are failing because the Spinner component does not
expose data-testid="loader2-icon" while the tests (Spinner.test.tsx) query that
id; update the Spinner component (Spinner in
src/renderer/components/ui/Spinner.tsx) to add data-testid="loader2-icon" to the
rendered icon element (the same element that receives className, style/size and
color props) so the test queries (screen.getByTestId('loader2-icon')) find the
element, ensuring the component still applies className, size, and color props
to that element.

In `@src/renderer/components/AgentSessionsBrowser.tsx`:
- Around line 732-739: The icon-only GhostIconButton used to trigger
clearViewingSession lacks accessible naming; update the GhostIconButton instance
(the one rendering ChevronLeft and calling clearViewingSession) to include an
accessible label by adding an ariaLabel (e.g., "Back" or "Close session") and a
title prop so screen readers and tooltips can announce the button; ensure the
label accurately describes the action (e.g., ariaLabel="Go back to session
list") and keep the existing onClick and styling unchanged.

In `@src/renderer/components/AgentSessionsModal.tsx`:
- Around line 449-457: The GhostIconButton used as the icon-only back control in
AgentSessionsModal lacks an accessible name; update the GhostIconButton
invocation (the instance rendering ChevronLeft inside AgentSessionsModal) to
include an accessible label (e.g., aria-label="Back" or aria-label="Go back to
sessions" and optionally title="Back") so assistive technology can announce the
control; keep the existing onClick behavior that calls setViewingSession(null)
and setMessages([]).
- Around line 601-604: The empty-state title in AgentSessionsModal currently
hardcodes “Claude” (the ternary setting for title when sessions.length === 0);
change it to a provider-agnostic message or use the modal's provider variable
(e.g., providerName or session.provider) so the text reads generically like "No
sessions found for this project" or interpolates the current provider
dynamically; update the title prop in the AgentSessionsModal render branch that
references sessions to use the generic string or the provider variable instead
of the literal "Claude".

In `@src/renderer/components/CreatePRModal.tsx`:
- Around line 233-235: The close button is an icon-only control
(GhostIconButton) with no accessible name; update the GhostIconButton instance
used for closing the CreatePRModal to include an accessible label (e.g.,
ariaLabel="Close create pull request modal" or aria-label="Close") so assistive
tech can identify it; add the prop to the GhostIconButton wrapping the X icon
(the onClose handler stays unchanged) and use a clear, concise label like
"Close" or "Close create PR modal".

In `@src/renderer/components/CreateWorktreeModal.tsx`:
- Around line 143-145: The close button is icon-only and lacks an accessible
name; update the GhostIconButton element (the instance with onClose and child X)
to provide an accessible label (e.g., add aria-label="Close" and a matching
title="Close" or include visually hidden text) so screen readers can announce
it; ensure the label text is concise and localized if needed and keep the
onClick handler (onClose) unchanged.

In `@src/renderer/components/DirectorNotes/DirectorNotesModal.tsx`:
- Around line 215-217: The close button rendered with GhostIconButton (wrapping
the X icon and invoking onClose) lacks an accessible name; update the
GhostIconButton usage to include an aria-label (e.g., aria-label="Close" or a
localized string like aria-label={t('close')}) and also add a title attribute if
a tooltip is desired so screen readers and sighted users get a clear label;
ensure you set the attribute on the GhostIconButton element (not on the X icon)
to preserve existing behavior.

In `@src/renderer/components/LeaderboardRegistrationModal.tsx`:
- Around line 801-803: The GhostIconButton used for closing (the element with
onClick={onClose} containing the <X /> icon) is icon-only and lacks an
accessible name; update the GhostIconButton invocation to provide an explicit
accessible label by adding an ariaLabel (or aria-label) prop such as "Close"
(and optionally a title prop) so screen readers can announce its purpose,
ensuring the button still uses theme.colors.textDim for styling.

In `@src/renderer/components/MainPanel/MainPanelHeader.tsx`:
- Around line 225-236: The GhostIconButton used to copy the branch name (the
component wrapping the Copy icon and calling copyToClipboard with
gitInfo.branch) relies only on the title prop for accessibility; add an explicit
ariaLabel prop (e.g., "Copy branch name") to GhostIconButton so the icon-only
control has a proper accessible name for screen readers, keeping the existing
title and click behavior intact.

In `@src/renderer/components/Settings/SshRemoteModal.tsx`:
- Around line 764-771: The icon-only GhostIconButton used to remove env vars
(the one wrapping <Trash2 /> and calling removeEnvVar(entry.id)) needs an
explicit accessible name: add an ariaLabel prop (or aria-label if that is the
component API) with a descriptive string such as "Remove variable" plus the
variable identifier (e.g., entry.key or entry.id) so screen readers can announce
which variable will be removed when the button is activated.

In `@src/renderer/components/ShortcutsHelpModal.tsx`:
- Around line 75-77: The GhostIconButton used for closing the ShortcutsHelpModal
is icon-only and lacks an accessible name; update the JSX for GhostIconButton
(the instance with props onClose and child <X className="w-4 h-4" />) to include
an ariaLabel (e.g., ariaLabel="Close shortcuts help" or similar) and optionally
a title prop with the same text so screen readers and tooltips have a clear
label; ensure the ariaLabel string is concise and descriptive of the control's
action.

In `@src/renderer/components/TransferProgressModal.tsx`:
- Around line 386-393: The close-handler currently uses onComplete?.() ||
onCancel(), which calls onCancel after onComplete because onComplete returns
void (falsy); change the handler in TransferProgressModal's GhostIconButton so
it conditionally calls onComplete if defined else calls onCancel (e.g., if
(onComplete) { onComplete(); } else { onCancel(); }) ensuring only one callback
runs when the close button is clicked; update references to onComplete and
onCancel in that GhostIconButton onClick prop accordingly.

In `@src/renderer/components/UpdateCheckModal.tsx`:
- Around line 195-197: The GhostIconButton rendering the close icon
(GhostIconButton with onClose and inner <X />) is missing an accessible label;
update the GhostIconButton usage in UpdateCheckModal to include an explicit
aria-label (e.g., "Close update modal") or ariaLabel prop (or title) so screen
readers can identify the control, ensuring the close action remains wired to
onClose and the icon (<X />) is unchanged.

In `@src/renderer/components/WorktreeConfigModal.tsx`:
- Around line 209-211: The close icon button in WorktreeConfigModal uses
GhostIconButton with only an <X /> icon and no accessible name; update the
GhostIconButton where it is rendered (the element with props onClick={onClose}
and child <X />) to include an explicit accessible name by adding an aria-label
(e.g., aria-label="Close") or a title prop so screen readers can announce it;
ensure the prop key matches GhostIconButton's API (aria-label or title) so
accessibility tools receive the label.

---

Outside diff comments:
In `@src/renderer/components/SymphonyModal.tsx`:
- Around line 712-719: The header for "Available Issues" contains an unreachable
loading indicator: remove the conditional JSX `{isLoadingIssues && <Spinner
size={12} color={theme.colors.accent} />}` in the h4 (the Available Issues
header) because `isLoadingIssues` is already routed to the skeleton path
earlier; update the component (SymphonyModal.tsx) to simply render the header
text and count (availableIssues.length) without that spinner conditional.

---

Nitpick comments:
In `@src/renderer/components/AutoRun/AutoRunExpandedModal.tsx`:
- Around line 422-424: The close button relies on a title for accessible naming;
update the GhostIconButton instance used with the onClick handler handleClose
and icon X to include an explicit ariaLabel prop (e.g., ariaLabel="Close") so
screen readers get a consistent name rather than depending on the tooltip/title;
ensure the ariaLabel string matches other close buttons in the component for
consistency.

In `@src/renderer/components/DocumentsPanel.tsx`:
- Around line 488-490: The close button is icon-only and needs an accessible
name: update the GhostIconButton usage (the one with onClose and the X icon) to
pass an explicit accessible label like aria-label="Close" (and optional
title="Close") so screen readers can announce the control; ensure the props are
added to the GhostIconButton component invocation that currently renders
<GhostIconButton onClick={onClose} color={theme.colors.textDim}> with the X icon
inside.

In `@src/renderer/components/PlaygroundPanel.tsx`:
- Around line 599-601: The close button in PlaygroundPanel is icon-only
(GhostIconButton with X) and needs an accessible name; update the
GhostIconButton instantiation in PlaygroundPanel.tsx to pass an accessible-name
prop (e.g., ariaLabel or aria-label depending on the GhostIconButton prop API)
such as "Close playground" and keep the existing onClose and color props so
assistive tech can announce the action reliably.

In `@src/renderer/components/SendToAgentModal.tsx`:
- Around line 18-19: The send CTA in SendToAgentModal mixes two loader
components—use the shared Spinner instead of Loader2 to standardize UI; update
the send button render path in the SendToAgentModal component to import and
render Spinner (already imported at top) wherever Loader2 is used (e.g., the
send CTA/submit handler render branch), remove the Loader2 usage and any related
imports, and ensure Spinner receives the same props (size/weight/className) used
by Loader2 so styling and behavior remain consistent.

In `@src/renderer/components/Settings/EnvVarsEditor.tsx`:
- Around line 194-201: The GhostIconButton used for removing env var entries
(the element rendering Trash2 with onClick={() => removeEntry(entry.id)}) is
icon-only and should include an accessible label; add an ariaLabel prop (e.g.,
ariaLabel="Remove variable") to the GhostIconButton so screen readers reliably
announce the action, keeping the existing title and styling intact and ensuring
the removeEntry(entry.id) handler remains unchanged.

In `@src/renderer/components/ui/GhostIconButton.tsx`:
- Around line 31-32: The GhostIconButton component currently allows an icon-only
button to be rendered without an accessible name; update the rendered element to
set aria-label to a fallback value by using the props ariaLabel || title (i.e.,
when ariaLabel is absent use title) so the button always has an accessible name,
and ensure the title prop is preserved on the element; locate GhostIconButton
and change where aria-label is assigned to use ariaLabel ?? title (or ariaLabel
|| title) for both occurrences referenced (around the ariaLabel prop and the
other usage).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6c87ed85-2ca9-404a-933f-773a4adb2b8e

📥 Commits

Reviewing files that changed from the base of the PR and between b37cd16 and eb0eb0b.

📒 Files selected for processing (76)
  • src/__tests__/renderer/components/ui/EmptyStatePlaceholder.test.tsx
  • src/__tests__/renderer/components/ui/GhostIconButton.test.tsx
  • src/__tests__/renderer/components/ui/Spinner.test.tsx
  • src/renderer/components/AboutModal.tsx
  • src/renderer/components/AgentCreationDialog.tsx
  • src/renderer/components/AgentPromptComposerModal.tsx
  • src/renderer/components/AgentSessionsBrowser.tsx
  • src/renderer/components/AgentSessionsModal.tsx
  • src/renderer/components/AutoRun/AttachmentImage.tsx
  • src/renderer/components/AutoRun/AutoRunExpandedModal.tsx
  • src/renderer/components/AutoRun/AutoRunSearchBar.tsx
  • src/renderer/components/AutoRun/AutoRunToolbar.tsx
  • src/renderer/components/BatchRunnerModal.tsx
  • src/renderer/components/CreatePRModal.tsx
  • src/renderer/components/CreateWorktreeModal.tsx
  • src/renderer/components/CueYamlEditor/CueAiChat.tsx
  • src/renderer/components/CueYamlEditor/CueYamlEditor.tsx
  • src/renderer/components/DebugPackageModal.tsx
  • src/renderer/components/DeleteWorktreeModal.tsx
  • src/renderer/components/DirectorNotes/AIOverviewTab.tsx
  • src/renderer/components/DirectorNotes/DirectorNotesModal.tsx
  • src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
  • src/renderer/components/DocumentGraph/DocumentGraphView.tsx
  • src/renderer/components/DocumentsPanel.tsx
  • src/renderer/components/EmptyStateView.tsx
  • src/renderer/components/FeedbackChatView.tsx
  • src/renderer/components/FileExplorerPanel.tsx
  • src/renderer/components/FilePreview/FilePreview.tsx
  • src/renderer/components/FilePreview/FilePreviewHeader.tsx
  • src/renderer/components/FilePreview/ImageViewer.tsx
  • src/renderer/components/FilePreview/MarkdownImage.tsx
  • src/renderer/components/GistPublishModal.tsx
  • src/renderer/components/GitWorktreeSection.tsx
  • src/renderer/components/GroupChatModal.tsx
  • src/renderer/components/History/HistoryStatsBar.tsx
  • src/renderer/components/InlineWizard/DocumentGenerationView.tsx
  • src/renderer/components/InlineWizard/WizardPill.tsx
  • src/renderer/components/LeaderboardRegistrationModal.tsx
  • src/renderer/components/MainPanel/AgentErrorBanner.tsx
  • src/renderer/components/MainPanel/BrowserTabView.tsx
  • src/renderer/components/MainPanel/MainPanelContent.tsx
  • src/renderer/components/MainPanel/MainPanelHeader.tsx
  • src/renderer/components/MarkdownRenderer.tsx
  • src/renderer/components/MarketplaceModal.tsx
  • src/renderer/components/MergeProgressModal.tsx
  • src/renderer/components/MergeProgressOverlay.tsx
  • src/renderer/components/MergeSessionModal.tsx
  • src/renderer/components/NewInstanceModal/AgentPickerGrid.tsx
  • src/renderer/components/NewInstanceModal/EditAgentModal.tsx
  • src/renderer/components/NotificationsPanel.tsx
  • src/renderer/components/PlaygroundPanel.tsx
  • src/renderer/components/PromptComposerModal.tsx
  • src/renderer/components/RightPanel.tsx
  • src/renderer/components/SendToAgentModal.tsx
  • src/renderer/components/SessionItem.tsx
  • src/renderer/components/SessionList/SessionList.tsx
  • src/renderer/components/Settings/EnvVarsEditor.tsx
  • src/renderer/components/Settings/SettingsSearch.tsx
  • src/renderer/components/Settings/SshRemoteModal.tsx
  • src/renderer/components/Settings/SshRemotesSection.tsx
  • src/renderer/components/ShortcutsHelpModal.tsx
  • src/renderer/components/SummarizeProgressModal.tsx
  • src/renderer/components/SummarizeProgressOverlay.tsx
  • src/renderer/components/SymphonyModal.tsx
  • src/renderer/components/ToolCallCard.tsx
  • src/renderer/components/TransferProgressModal.tsx
  • src/renderer/components/UpdateCheckModal.tsx
  • src/renderer/components/Wizard/screens/PhaseReviewScreen.tsx
  • src/renderer/components/Wizard/shared/DocumentEditor.tsx
  • src/renderer/components/WorktreeConfigModal.tsx
  • src/renderer/components/shared/AgentConfigPanel.tsx
  • src/renderer/components/ui/EmptyStatePlaceholder.tsx
  • src/renderer/components/ui/GhostIconButton.tsx
  • src/renderer/components/ui/Modal.tsx
  • src/renderer/components/ui/Spinner.tsx
  • src/renderer/components/ui/index.ts

Comment thread src/__tests__/renderer/components/ui/Spinner.test.tsx
Comment thread src/renderer/components/AgentSessionsBrowser.tsx
Comment thread src/renderer/components/AgentSessionsModal.tsx
Comment thread src/renderer/components/AgentSessionsModal.tsx
Comment thread src/renderer/components/CreatePRModal.tsx Outdated
Comment thread src/renderer/components/Settings/SshRemoteModal.tsx
Comment thread src/renderer/components/ShortcutsHelpModal.tsx Outdated
Comment thread src/renderer/components/TransferProgressModal.tsx
Comment thread src/renderer/components/UpdateCheckModal.tsx Outdated
Comment thread src/renderer/components/WorktreeConfigModal.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/renderer/components/AgentSessionsBrowser.tsx (1)

741-761: Add ariaLabel to remaining icon-only GhostIconButton controls.

These buttons currently rely on title only. Adding ariaLabel keeps accessible naming explicit and consistent across icon-only actions.

♿ Suggested tweak
 							<GhostIconButton
 								onClick={(e) => toggleStar(viewingSession.sessionId, e)}
 								padding="p-1.5"
+								ariaLabel={
+									starredSessions.has(viewingSession.sessionId)
+										? 'Remove from favorites'
+										: 'Add to favorites'
+								}
 								title={
 									starredSessions.has(viewingSession.sessionId)
 										? 'Remove from favorites'
 										: 'Add to favorites'
 								}
 							>
@@
 										<GhostIconButton
 											onClick={(e) => {
 												e.stopPropagation();
 												setRenamingSessionId(viewingSession.sessionId);
 												setRenameValue(viewingSession.sessionName || '');
 												setTimeout(() => renameInputRef.current?.focus(), 50);
 											}}
 											padding="p-0.5"
+											ariaLabel="Rename session"
 											title="Rename session"
 										>
@@
 										<GhostIconButton
 											onClick={(e) => {
 												e.stopPropagation();
 												setRenamingSessionId(viewingSession.sessionId);
 												setRenameValue('');
 												setTimeout(() => renameInputRef.current?.focus(), 50);
 											}}
 											padding="p-0.5"
+											ariaLabel="Add session name"
 											title="Add session name"
 										>

Also applies to: 799-810, 821-832

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentSessionsBrowser.tsx` around lines 741 - 761, The
GhostIconButton instances used for starring sessions (the component rendering
Star and calling toggleStar with viewingSession.sessionId and using title prop)
lack explicit accessible names; add an ariaLabel prop to each icon-only
GhostIconButton (set it to the same string as the title, i.e., "Add to
favorites" or "Remove from favorites" based on
starredSessions.has(viewingSession.sessionId)) so screen readers receive an
explicit label; apply the same change to the other GhostIconButton occurrences
handling session actions in the same file that use icon-only buttons.
src/renderer/components/TransferProgressModal.tsx (1)

387-393: Optional: reuse existing handler to avoid callback-branch duplication.

This branch is repeated in multiple places; wiring this button to handlePrimaryClick (or a shared helper) would reduce drift risk.

♻️ Minimal dedup option
 					{isComplete && (
 						<GhostIconButton
-							onClick={() => {
-								if (onComplete) {
-									onComplete();
-								} else {
-									onCancel();
-								}
-							}}
+							onClick={handlePrimaryClick}
 							ariaLabel="Close modal"
 							color={theme.colors.textDim}
 						>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/TransferProgressModal.tsx` around lines 387 - 393,
The click handler duplicates logic present in handlePrimaryClick; replace the
inline onClick block in TransferProgressModal (currently calling onComplete() or
onCancel()) with a call to handlePrimaryClick to reuse the shared behavior and
avoid branching duplication—ensure handlePrimaryClick is accessible in the
component scope and that it performs the same onComplete/onCancel decision so
behavior remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/renderer/components/AgentSessionsBrowser.tsx`:
- Around line 741-761: The GhostIconButton instances used for starring sessions
(the component rendering Star and calling toggleStar with
viewingSession.sessionId and using title prop) lack explicit accessible names;
add an ariaLabel prop to each icon-only GhostIconButton (set it to the same
string as the title, i.e., "Add to favorites" or "Remove from favorites" based
on starredSessions.has(viewingSession.sessionId)) so screen readers receive an
explicit label; apply the same change to the other GhostIconButton occurrences
handling session actions in the same file that use icon-only buttons.

In `@src/renderer/components/TransferProgressModal.tsx`:
- Around line 387-393: The click handler duplicates logic present in
handlePrimaryClick; replace the inline onClick block in TransferProgressModal
(currently calling onComplete() or onCancel()) with a call to handlePrimaryClick
to reuse the shared behavior and avoid branching duplication—ensure
handlePrimaryClick is accessible in the component scope and that it performs the
same onComplete/onCancel decision so behavior remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7c6a6d9f-1b25-4267-8b86-d11fe6a2095b

📥 Commits

Reviewing files that changed from the base of the PR and between eb0eb0b and 912fd74.

📒 Files selected for processing (12)
  • src/renderer/components/AgentSessionsBrowser.tsx
  • src/renderer/components/AgentSessionsModal.tsx
  • src/renderer/components/CreatePRModal.tsx
  • src/renderer/components/CreateWorktreeModal.tsx
  • src/renderer/components/DirectorNotes/DirectorNotesModal.tsx
  • src/renderer/components/LeaderboardRegistrationModal.tsx
  • src/renderer/components/MainPanel/MainPanelHeader.tsx
  • src/renderer/components/Settings/SshRemoteModal.tsx
  • src/renderer/components/ShortcutsHelpModal.tsx
  • src/renderer/components/TransferProgressModal.tsx
  • src/renderer/components/UpdateCheckModal.tsx
  • src/renderer/components/WorktreeConfigModal.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/renderer/components/CreateWorktreeModal.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/renderer/components/ShortcutsHelpModal.tsx
  • src/renderer/components/DirectorNotes/DirectorNotesModal.tsx
  • src/renderer/components/CreatePRModal.tsx
  • src/renderer/components/MainPanel/MainPanelHeader.tsx
  • src/renderer/components/LeaderboardRegistrationModal.tsx
  • src/renderer/components/AgentSessionsModal.tsx

@jSydorowicz21 jSydorowicz21 added refactor Clean-up needs ready to merge This PR is ready to merge labels Apr 15, 2026
@jSydorowicz21 jSydorowicz21 force-pushed the dedup/phase-08-shared-ui-components branch from b6e6d60 to 24b840c Compare April 15, 2026 05:47
jSydorowicz21 and others added 4 commits April 16, 2026 12:44
- 08A: Extract GhostIconButton (src/renderer/components/ui/GhostIconButton.tsx)
  encapsulating the "p-N rounded hover:bg-white/10 transition-colors" icon-button
  pattern. Migrated 54 call sites across 43 files via AST-aware codemod.
- 08B: Extract Spinner (src/renderer/components/ui/Spinner.tsx) for the
  Loader2 + animate-spin loading indicator pattern. Migrated 84 call sites
  across 55 files and removed 42 now-unused Loader2 imports.
- 08C: Add EmptyStatePlaceholder (src/renderer/components/ui/EmptyStatePlaceholder.tsx)
  for the generic "No X" icon + title + description pattern. Adopted at 2 call
  sites (AgentSessionsBrowser, AgentSessionsModal). Distinct from the
  top-level EmptyStateView welcome screen.

Added minimal vitest coverage for each new primitive (13 tests total).
All three components exported from src/renderer/components/ui/index.ts.

Net impact: 70 files changed, 404 insertions(+), 582 deletions(-).
- Fix onComplete?.() || onCancel() always calling both (void is falsy)
- Add ariaLabel to icon-only GhostIconButton instances across 11 modals
- Remove hardcoded "Claude" from AgentSessionsModal empty state
@jSydorowicz21 jSydorowicz21 force-pushed the dedup/phase-08-shared-ui-components branch from 8d292a0 to d96adf6 Compare April 16, 2026 17:45
@reachrazamair reachrazamair merged commit 5f7b218 into RunMaestro:rc Apr 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready to merge This PR is ready to merge refactor Clean-up needs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants